#pragma once

#include <vector>
#include "path.hpp"
#if defined (__linux__) || defined (__APPLE__)
#include "dirent.h"
#elif defined (_WIN32)  || defined (_WIN64)
#include "direct.h"
#endif

namespace easyfs
{
    inline bool exists(const path& p) 
    { 
        struct stat st;
        return stat(p.c_str(), &st) == 0;
    }

    inline void create_directory(const path& path, size_t pos = 0)
    {
        static auto makedir = [](const class path& p) 
        {
            if (exists(p))
                return;
#if defined (_WIN32)  || defined (_WIN64)
            if (_mkdir(p.c_str()) != 0)
#else
            if (mkdir(p.c_str(), 0775) != 0)
#endif
                throw std::runtime_error("easyfs create_directory error: mkdir failed");
        };

        pos = path.string().find(path_sepr::sepr, pos);

        if (pos == string_type::npos)
        {
            makedir(path);
        }
        else
        {
            makedir(path.string().substr(0, pos));
            create_directory(path, pos + 1);
        }
    }

    inline bool is_directory(const path& p)
    {
        struct stat st;
        if (stat(p.c_str(), &st) != 0) 
            throw std::runtime_error("easyfs is_directory error: file doesn't exists");
#if defined (__linux__) || defined (__APPLE__)
        return S_ISDIR(st.st_mode);
#elif defined (_WIN32)  || defined (_WIN64)
        return st.st_mode & S_IFDIR;
#endif
    }
    
    inline bool is_file(const path& p)
    {
        struct stat st;
        if (stat(p.c_str(), &st) != 0)
            throw std::runtime_error("easyfs is_file error: file doesn't exists");
#if defined (__linux__) || defined (__APPLE__)
        return S_ISREG(st.st_mode);
#elif defined (_WIN32)  || defined (_WIN64)
        return st.st_mode & S_IFREG;
#endif
    }

    // equivalent to the command "mv"
    inline void move(const path& src, const path& dst)
    {
        static auto move_file = [](const path& src, const path& dst)
        {
#if defined (_WIN32)  || defined (_WIN64)
            if (!MoveFileA(src.c_str(), dst.c_str()))
#elif defined (__linux__) || defined (__APPLE__)
            if (rename(src.c_str(), dst.c_str()) != 0)
#endif
                throw std::runtime_error("easyfs rename error");
        };

        // if dst exists, then it must be a directory, 
        // otherwise, it must be a file
        if (exists(dst)) 
        {
            move_file(src.c_str(), (dst / src.filename()).c_str());
        }
        else
        {
            if (is_directory(src))
                throw std::runtime_error("easyfs move error: error file type");
            move_file(src, dst);
        }
    }

    inline int64_type file_size(const path& p)
    {
        struct stat st;
        if (stat(p.c_str(), &st) == -1)
            throw std::runtime_error("easyfs file_size error");
        return st.st_size;
    }

    inline string_type read_file(const path& path, size_t pos = 0, size_t len = std::string::npos)
    {
        std::ifstream ifs(path.string());
        if (!ifs.is_open())
            throw std::runtime_error("easyfs read_file error");

        string_type res;

        if (len == std::string::npos)
            len = file_size(path);

        res.resize(len);

        ifs.seekg(pos, std::ios::beg);
        ifs.read(&res[0], len);

        return res;
    }
   
    inline void write_file(const path& path, const string_type content)
    {
        std::ofstream ofs(path.string());
        if (!ofs.is_open())
            throw std::runtime_error("easyfs create_file error");

        ofs << content;
    }

    inline void create_file(const path& path)
    {
        create_directory(path.parent_path());

        std::ofstream ofs(path.string());
        if (!ofs.is_open())
            throw std::runtime_error("easyfs create_file error");
    }

    // if you need the prefix of the parent directory before filename,
    // you need to set the flag 'need_parent_path' to true.
    // such as: test.cc -> './test.cc'
    //          test.cc -> 'file_system/test.cc'
    inline std::vector<path> scan_directory(const path& p, bool need_parent_path = false)
    {
        if (!exists(p))
            throw std::runtime_error("easyfs scan_directory error");

#if defined (__linux__) || defined (__APPLE__)

        struct dirent** list = nullptr;

        int n = scandir(p.c_str(), &list, [](const struct dirent* entry) ->int 
        {
            if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) return 0;
            else return 1;
        } , nullptr);

        if (n < 0)
            throw std::runtime_error("easyfs scan_directory error");

        std::vector<path> v;
        for (int i = 0; i < n; ++i)
        {
            // add the parent directory prefix to the scanned file name
            if (need_parent_path)
                v.push_back(p / path(list[i]->d_name));
            else 
                v.push_back(path(list[i]->d_name));

            free(list[i]);
        }
        free(list);

#elif defined (_WIN32)  || defined (_WIN64)
        WIN32_FIND_DATAA data;
        HANDLE handle = FindFirstFileA((p / "/*").c_str(), &data);

        if (handle == INVALID_HANDLE_VALUE)
            throw std::runtime_error("easyfs scan_directory error");

        std::vector<path> v;
        do 
        {
            std::string file = data.cFileName;
            if (file == "." || file == "..")
                continue;

            // add the parent directory prefix to the scanned file name
            if (need_parent_path)
                v.push_back(p / path(file));
            else
                v.push_back(path(file));
        } 
        while (FindNextFileA(handle, &data));

        FindClose(handle);
#endif
        return v;
    }

    // recursive scanning of the entire directory
    inline std::vector<path> recursive_scan_directory(const path& root_path)
    {
        if (!exists(root_path))
            throw std::runtime_error("easyfs recursive_scan_directory error");

        auto paths = scan_directory(root_path, true);

        // The insert inside the traversal will increase the size of paths, 
        // therefore iterators will be invalid if you choose it.
        //
        // This kind of code is not standardized enough, 
        // but I can't think of a better way just now.

        // for (auto& path : paths) // dangerous

        int size = paths.size();
        for (int i = 0; i < size; ++i)
        {
            const auto& path = paths[i];

            if (is_directory(path))
            {
                auto rps = recursive_scan_directory(path);
                paths.insert(paths.end(), rps.begin(), rps.end());
            }
        }

        return paths;
    }

    // remove a file or an empty directory
    inline void remove(const path& p)
    {
        if (is_file(p))
        {
            if (std::remove(p.c_str()) != 0)
                throw std::runtime_error("easyfs remove error");
        }
        else if (is_directory(p))
        {
#if defined (_WIN32)  || defined (_WIN64)
            if (!RemoveDirectoryA(p.c_str()))
#else
            if (rmdir(p.c_str()) != 0)
#endif
                throw std::runtime_error("easyfs remove error: directory is not empty");
        }
        else
        {
            throw std::runtime_error("easyfs remove error: unknown file type");
        }
    }

    // This function is only called by other function, so you don't use it.
    // You can use remove, remove_all, clear.
    inline void recursive_remove(const path& path, bool rm_entry_dir = false)
    {
        if (is_directory(path))
        {
            auto ps = scan_directory(path, true);

            for (auto& p : ps)
            {
                recursive_remove(p, true); // each recursive call's delete flag is set to true, 
                                           // causing it only affecting the entry directory.
            }

            if (rm_entry_dir) remove(path);
        }
        else
        {
            remove(path);
        }
    }

    inline void remove_all(const path& path) { recursive_remove(path, true); }

    inline void clear(const path& path)
    {
        if (is_file(path))
        {
            create_file(path); // open a existed file will empty the file
        }
        else if (is_directory(path)) 
        {
            recursive_remove(path, false);
        }
    }

    // only copy file to file and directory to directory
    inline void copy(const path& src, const path& dst)
    {
        if (is_file(src))
        {
            create_file(dst);
            write_file(dst, read_file(src));
        }
        else if (is_directory(src))
        {
            create_directory(dst);
            std::vector<path> ps = recursive_scan_directory(src);

            create_directory(dst / src); // firstly create the root path

            for (auto p : ps)
            {
                if (is_file(p)) 
                {
                    create_file(dst / p); // create the directories are located in path.
                    copy(p, dst / p); // copy file to file
                }
                else if (is_directory(p))
                {
                    create_directory(dst / p); // copy directory to directory
                }
                else
                {
                    throw std::runtime_error("easyfs copy error: unknown file type");
                }
            }
        }
    }

    inline int64_type line_number(const path& path)
    {
        std::ifstream ifs(path.string());
        if (!ifs.is_open())
            throw std::runtime_error("easyfs read_file error");

        int64_type cnt = 0;

        char ch = 0;
        while (ifs.get(ch))
            if (ch == '\n') ++cnt;

        return cnt;
    }

    inline time_type last_modify_time(const path& path)
    {
        struct stat st;
        if (stat(path.c_str(), &st) == -1)
            throw std::runtime_error("easyfs last_modify_time error");
        return st.st_mtime;
    }

    inline time_type last_access_time(const path& path)
    {
        struct stat st;
        if (stat(path.c_str(), &st) == -1)
            throw std::runtime_error("easyfs last_access_time error");
        return st.st_atime;
    }

    inline time_type last_change_time(const path& path)
    {
        struct stat st;
        if (stat(path.c_str(), &st) == -1)
            throw std::runtime_error("easyfs last_change_time error");
        return st.st_ctime;
    }
}
